2주차 - 네트워크
개요
미리 궁금점
ip 명령어로 네트워크 네임스페이스를 조작할 수 없나?
lsns, nsenter 없이말이다.
-> 해보니 ip netns를 이용하면 된다.
aws cni 말고 다른 것을 사용할 수도 있나.
https://hanhorang.tistory.com/34
llm 서빙 관련 내용. 나중에 정리하자
토폴로지 게아 프록시모드
추가로 해볼 것.
기본 설치
앤서블 쓸 때, 아마존 리눅스 2023은 인터페이스 이름이 다르니 주의하하자.
vpc cni
ipam은 ip를 관리하는 풀
이것은 fast pod startup을 지원한다.
미리 세컨더리 보조 ip를 가져와 있다.
하나의 eni당 5개, 해서 10개가 확인된다.
만약 받을 수 있는 게 부족하면 eni를 더 붙여준다.
대충 예시 파드를 하나 만들어보고 확인해보자.
여기에서 192.168.1.112 파드는 어떻게 통신이 될 수 있을까?
해당 노드에 들어가서 ip addr
, ip route
를 했을 때의 값이다.
112 ip로 가는 통신은 eni77~로 가도록 되어 있다.
그럼 이 인터페이스는 또 어디로 이어지는지 확인해보자.
lsns -t net
lsns를 통해 현 호스트이 네임스페이스를 확인할 수 있다.
이때 type을 지정해서 네트워크 네임스페이스를 확인해보면..
현재 띄워져있는 파드의 pause container 프로세스가 보인다!
export PID=$(lsns -t net | grep pause | awk '{print $4}')
nsenter -t $PID --net ip addr
nsenter -t $PID --net ip route
프로세스 id까지 알았으니, 이제 현 터미널을 해당 네임스페이스를 사용하게 만들 수 있다.
ip link
nsenter -t $PID --net ip link
그럼 이제 어떤 인터페이스가 서로 연결됐는지도 확인할 수 있다.
루트 네임스페이스에서 eni22bc~는 link-netns로 cni-23~로 연결된다는 것을 알 수 있으며, 파드의 네임스페이스에선 id 0으로 연결이 되는 것이 확인된다.
ip netns list
이렇게 ip 명령어를 통해 해서 보는 방법이 있다(물론 위의 lsns로도 확인 가능하다).
이렇게 컨테이너로 어떻게 통신이 이뤄지는지 비로소 알 수 있다.
실제 통신 확인
이번에는 실제로 패킷이 어떻게 전달되는지 직접적으로 확인해보자.
apiVersion: v1
kind: Pod
metadata:
name: test-pod
labels:
app: target
spec:
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
nodeName: ip-192-168-1-61.ap-northeast-2.compute.internal
---
apiVersion: v1
kind: Pod
metadata:
name: inside-node
spec:
containers:
- image: nicolaka/netshoot
name: test
command:
- sh
- -c
- "sleep infinity"
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- target
topologyKey: "kubernetes.io/hostname"
---
apiVersion: v1
kind: Pod
metadata:
name: inter-node
spec:
containers:
- image: nicolaka/netshoot
name: test
command:
- sh
- -c
- "sleep infinity"
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- target
topologyKey: "kubernetes.io/hostname"
테스트용 nginx를 띄운 뒤, Affinity 세팅을 통해 한 파드는 같은 노드에, 다른 하나는 다른 노드에 띄워지도록 만들었다.
각각의 파드의 ip는 이렇게 할당됐다.
k exec -ti inside-node -- zsh
이런 식으로 각 파드에 들어가서 통신을 해볼 것이다.
tcpdump -nn -i any dst port 80
이런 식으로 패킷을 추적한다.
노드 내부 통신
테스트 파드가 있는 노드에서 먼저 추적해본다.
tcpdump -nn -i any dst 192.168.1.96
test-pod는 96으로 끝나는 ip를 가지고 있으니 파드로 가는 요청을 한번 보자.
97로 끝나는 inside-node 파드에서 요청이 날아갔다.
참고로 eni 인터페이스는 inside-node, test-pod에게 붙은 네트워크 인터페이스이다.
서로가 서로에게 바로바로 통신을 주고받는 것이 확인된다.
사진으로 싣지는 않겠지만, inside node의 파드의 eth0에서 트래픽이 나갈 때는 97로 나가는데, 이후 노드의 라우팅 테이블에서 96이라는 ip에 대해 eni7fba~로 가도록 되어 있었기 때문에 자연스럽게 별다른 트래픽 이동없이 바로 연결이 된 것이다.
노드 간 통신
k exec -ti inter-node -- zsh
이번에는 다른 노드에서 요청을 보낼 것이다.
이번에도 2.96, 즉 해당 파드의 ip를 달고 통신이 이뤄진 것을 확인할 수 있다.
그러나 이것은 얼핏 보면 조금 이상하다.
노드를 나가는 트래픽이 다시 돌아오기 위해서는 통상적으로 노드의 IP를 소스로 가지는 식으로, 즉 SNAT를 해서 나가야만 제대로 돌아올 수 있다.
그래서 흔히 노드의 ip가 추적됐어야 할 것만 같다.
그런데 위의 결과는 소스가 파드의 ip를 그대로 가지고 있는 모습이다.
iptables --list -t nat -n | grep AWS -A1 -B3
해당 답의 힌트는 iptables에서 얻을 수 있다.
트래픽이 나가기 직전에 적용되는 POSTROUTING 체인을 보면 쿠버 클러스터에 흔히 세팅되는 KUBE-POSTROUTING외에 AWS-SNAT-CHAIN-0을 볼 수 있다.
그리고 해당 체인은 192.168.0.0/16
에 대해서 RETURN을 하므로, 그대로 POSTROUTING으로 돌아가게 되고 결국 SNAT 없이 트래픽은 인터페이스를 타고 나가게 된다.
ip route get 192.168.1.96
참고로 해당 경로를 나갈 때는..
이렇게 ens5라는, 인스턴스 바깥으로 나가는 경로를 탄다.
src 힌트가 붙어 있어서 무조건 snat이 될 거라고 생각했지만, 막상 규칙으로는 snat 없이 게이트웨이로 가게 되는 것이다.
그래서 결과적으로 vpc cni는 클러스터 내부에서 모두 자신의 ip를 달고 통신을 하게 된다.
watch -d 'sudo iptables -v --numeric --table nat --list AWS-SNAT-CHAIN-0; echo ; sudo iptables -v --numeric --table nat --list KUBE-POSTROUTING; echo ; sudo iptables -v --numeric --table nat --list POSTROUTING'
참고로 이런 식으로 watch를 해두면 패킷이 나가는 개수도 확인할 수 있다.
노드 외부로 통신
ping google.com
마지막으로 외부로 통신이 나가는 상황을 본다.
테스트는 test-pod가 있는 위치에서 진행했다.
tcpdump -nn -i any icmp
이번에는 파드에서 나가는 요청이 SNAT되어 ens5 인터페이스를 탈 때는 노드의 ip가 찍히는 것을 확인할 수 있다.
위의 iptables(위 그림은 다른 노드에서 찍은 거지만 아무튼..)에서 RETURN되지 않고 SNAT 규칙에 걸렸기 때문이다.
SNAT 비활성화
당연히 SNAT되는 게 정상적인 것 같지만, 이를 비활성화하는 것도 가능하다.
만약 vpc 피어링을 하고 있는 경우라면 어차피 서로의 대역에 대해 경로를 알고 있으므로 비활성되더라도 통신에 문제는 없을 것이다.
먼저 확인을 위해 운영 호스트 보안 그룹에 eks 클러스터 보안그룹에서의 ping을 허용하도록 세팅했다.
k exec -ti inside-node -- ping 172.20.0.25
파드에서 운영 호스트로 ping을 날리니 워커 노드의 ip로 값이 들어오는 것이 확인된다.
kubectl set env daemonset aws-node -n kube-system AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS=172.20.0.0/16
cni에 이렇게 환경 변수를 설정해주면, snat를 하지 않을 대역을 지정할 수 있다.
다시 ping을 날리면, 이번에는 파드의 ip가 그대로 찍히는 것이 확인된다!
참고로 1.8 버전 이전의 vpc cni는 secondary 네트워크 인터페이스에서 ip를 할당 받은 파드의 경우에 대해 들어오고 나가는 인터페이스 경로가 달라서 rp_filter에 의해 패킷이 드랍되는 경우가 있었다고 한다.[1]
참고로 vpc cni에 대해 세팅한 환경 변수 값들을 지속적으로 쓰고 싶다면 이렇게 세팅 설정에 넣어주면 된다.
로드 밸런서
그냥 로드밸런서를 만들면 nlb, 인그레스를 만들면 alb.
기본 인그레스는 라운드로빈
nlb는 기본적으로 소스 ip를 파드로 전달하지 않는다.
만약 도메인을 못쓸 거라면 nlb가 낫다.
어노테이션으로 각종 설정을 한다.
이 각각의 모드에 대해서 실습해보자.
alb loadbalancer는 두 가지 유형을 사용할 수 있다.
ip와 인스턴스. 이걸 활용하는 두 모드가 있는 거다.
인스턴스 모드는 다른 az로 갈 수도 있다는 것.
거기에 파드의 모니터링도 불가능
반면 ip 모드는 iptables 전부 건너뛰고 간다.
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
파드레디네스 게이트
https://docs.aws.amazon.com/eks/latest/best-practices/load-balancing.html
파드의 헬스 상태는 큐블렛이 담당한다.
ip모드를 쓰면, 컨트롤러가 elb api를 써서 타겟의 상태를 챙겨준다.
롤링 업데이트를 할 때, 엔포슬이 업데이트되는게 aws에서 그룹 업데이트 되는 것보다 빠르다.
이짧으 ㄴ순간에 클라의 요청이 드랍될 수 있따.
이때 필요한게 파드 레디네스 게이트.
파드의 컨디션에 달린다.
컨트롤러가 elb에 실제로 올라가고 그쪽 헬스체크로도 건강하다고 나와야지만 이것을 레디로 돌린다.
외부의 상태까지 따져 따라 파드의 조건을 결정짓기에 안전하 트래픽을 보장한다.
로드밸런서는 타겟그룹으로 IP 모드를 제공하는데, 이걸 이용하는 것이다.
그래서 파드의 ip를 바로 타겟으로 삼아준다.
기본 로드 밸런서 만들기
간단 배포
helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=$CLUSTER_NAME
설치 자체는 참 간단하다.
k describe deployments.apps -n kube-system aws-load-balancer-controller
어떤 식으로 배포됐는지도 확인해본다.
컨트롤러 컨테이너에는 인자로 현재 클러스터 네임과, 인그레스 클래스가 될 이름이 들어가있다.
서비스 어카운트는 aws-load-balancer-controller
이고, 이 친구가 aws의 로드밸런서를 만들 수 있는 권한이 있어야만 제대로 동작한다.
그 권한은 IRSA로 세팅되는데, 제대로 설정이 된다면 서비스 어카운트의 어노테이션에 eks.amazonaws.com/role-arn
가 붙어있어야 한다.
이렇게 인그레스 클래스를 확인할 수 있다.
kubectl describe clusterrolebindings.rbac.authorization.k8s.io aws-load-balancer-controller-rolebinding
kubectl describe clusterroles.rbac.authorization.k8s.io aws-load-balancer-controller-role
해당 어카운트가 어떤 롤을 가지고 있는지도 확인해보자.
네트워크와 관련된 다양한 권한을 가지고 있는 것이 확인된다.
이렇게 클러스터 내부의 리소스를 컨트롤러가 접근하여 각종 작업을 해줄 수 있을 것이다.
클러스터 IRSA 설정
그럼 이제 클러스터 외부에 컨트롤러가 조작을 할 수 있도록 iRSA를 세팅해보자.[2]
안타깝게도.. 이 친구들은 폴리시 자체는 문서에서 다운 받을 수 있게 해뒀지만 아예 공식적으로 제공해주지는 않는다.
curl -O https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.11.0/docs/install/iam_policy.json
그래서 이걸 받아서 직접 정책을 만들고, 이걸 롤에 붙여주는 작업을 직접 해줘야 한다.[3]
aws iam create-policy \
--policy-name AWSLoadBalancerControllerIAMPolicy \
--policy-document file://iam_policy.json
정책을 미리 만들어서 올려둔다.
eksctl create iamserviceaccount \
--cluster=my-cluster \
--namespace=kube-system \
--name=aws-load-balancer-controller \
--role-name AmazonEKSLoadBalancerControllerRole \
--attach-policy-arn=arn:aws:iam::111122223333:policy/AWSLoadBalancerControllerIAMPolicy \
--approve
그 다음에 eksctl을 사용할 경우, 이렇게 서비스어카운트에 대해 어떤 롤을 붙여줄지를 지정한다.
이때 해당 서비스어카운트에 연결될 롤과 정책을 한번에 지정해버린다.
직접 서비스 어카운트에 해당 값을 넣어본다.
그러나 vpc id에 접근할 수 없다는 에러가 떴고, 이를 직접적으로 넣어주는 방법을 시도했다.
인스턴스의 IMDSv2를 쓰는 것도 가능하지만, 설정이 되려 복잡해진다고 판단했다.
helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=$CLUSTER_NAME --set vpcId=$VPCID --set region=ap-northeast-2
컨트롤러를 담당하는 디플로이먼트에 직빵으로 인자를 수정하는 것도 가능하겠으나, 환경 유지 편의성을 위해 이렇게 세팅했다.
그러나 단순히 이렇게하면 문제가 생기는데, 헬름 차트에서 sa를 만들도록 할 경우 원하는 어노테이션이 제대로 부착되지 않기 때문이다.
그래서 k edit sa
를 해서 직접적으로 붙이면 최소한 이 에러는 해결된다.
그런데 또! 막상 컨트롤러가 잘 돌아간다고 해도 주의해야 한다.
sa가 제대로 반영된 채로 컨트롤러가 동작하지 않으면 이렇게 서비스 쪽에서 에러가 발생한다.
sa가 파드 생성 시점에 한번 볼륨으로 만들어져 마운팅되기에, 알아서 변경이 파드에 반영되지 않는다.
제대로 변경해주면 이렇게 성공한다!
그럼 이제 헬름 설정을 할 때 조금 더 제대로 세팅을 할 수 있게 해보자.
롤을 어노로 붙이려면 이 세팅을 하면 될 것이다.
아니면 보통 대중적으로 하는 방식은, 미리 sa를 직접 커스텀해서 만든 후에 여기에서 create=false를 하는 것이다.
내 생각에는 그냥 여기에서 해당 롤을 어노테이션으로 붙이게 하는 것도 괜찮을 것 같다.
여기에 매번 붙이는 방식은 귀찮으니, 이제 테라폼으로 헬름까지 세팅해야겠다.
locals {
lbc_name_in_aws = "AWSLoadBalancerController"
lbc_name_in_cluster = "aws-load-balancer-controller"
lbc_namespace = "kube-system"
}
provider "helm" {
kubernetes {
config_path = "./kubeconfig"
}
}
resource "helm_release" "lbc" {
name = local.lbc_name_in_cluster
repository = "https://aws.github.io/eks-charts"
chart = local.lbc_name_in_cluster
namespace = local.lbc_namespace
set {
name = "clusterName"
value = module.eks.cluster_name
}
set {
name = "region"
value = data.aws_region.current.name
}
set {
name = "vpcId"
value = module.eks_vpc.vpc_id
}
set {
name = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn"
value = aws_iam_role.lbc.arn
}
}
data "http" "lbc" {
url = "https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.11.0/docs/install/iam_policy.json"
}
resource "aws_iam_policy" "lbc" {
name = "${local.lbc_name_in_aws}IAMPolicy"
description = "IAM Policy for AWS Load Balancer Controller"
policy = data.http.lbc.response_body
}
data "aws_iam_policy_document" "lbc" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [module.eks.oidc_provider_arn]
}
condition {
test = "StringEquals"
variable = "${replace(module.eks.cluster_oidc_issuer_url, "https://", "")}:sub"
values = [
"system:serviceaccount:${local.lbc_namespace}:${local.lbc_name_in_cluster}",
]
}
effect = "Allow"
}
}
resource "aws_iam_role" "lbc" {
name = "${local.lbc_name_in_aws}IAM"
assume_role_policy = data.aws_iam_policy_document.lbc.json
}
resource "aws_iam_role_policy_attachment" "lbc" {
role = aws_iam_role.lbc.name
policy_arn = aws_iam_policy.lbc.arn
}
간단하게 코드를 설명하자면, 일단 문서에 나온 최소 기준으로 json파일을 가져와서 그대로 정책을 만든다.
이후에 롤까지 만드는데, 이때 assume을 할 수 있는 롤을 명시할 때는 해당 롤을 받을 수 있는 주체는 eks 클러스터의 OIDC로부터 인증을 받은 주체만 가능하도록 한다.
이를 체크하는 것이 sts:AssumeRoleWithWebIdentity
이고, identifiers에는 oidc 프로바이더를 명시해주면 된다.
확실치 않은 부분으로, 프로바이더의 arn을 넣어주는데 외부 oidc를 사용할 수 없는 것인지 잘 모르겠다.
아무튼 이를 통해 프로바이더에게 받은 토큰에 sub 클레임을 확인하여 로드밸런서 컨트롤러의 서비스 어카운트가 가질 값을 가지는지 확인한다.
이를 통과한 주체라면 로밸컨이 가져야 하는 정책을 활용할 수 있게 된다.
또한 헬름 리소스에서는 알아서 서비스 어카운트가 만들어지고, 이 어카운트의 어노테이션에 연결될 롤의 arn을 넣음으로써 IRSA가 성립하게 된다.
로드 밸런서 간단 테스트
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy-echo
spec:
replicas: 2
selector:
matchLabels:
app: deploy-websrv
template:
metadata:
labels:
app: deploy-websrv
spec:
terminationGracePeriodSeconds: 0
containers:
- name: aews-websrv
image: k8s.gcr.io/echoserver:1.5
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: svc-nlb-ip-type
annotations:
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "8080"
service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
type: LoadBalancer
loadBalancerClass: service.k8s.aws/nlb
selector:
app: deploy-websrv
로드 밸런서로서 쓸 때는 위와 같이 어노테이션을 달아준다.
(아래에서 볼 인그레스와 어노테이션 양식이 다르니 차이를 명심하자.)
서비스에 loadBalancerClass를 지정하는 것이 보이는데, 이 값은 어차피 넣지 않아도 Admission Webhook을 통해 알아서 주입된다.
콘솔이나 k get svc
로 나온 dns 이름을 확인하고 브라우저로 접속한 결과이다.
기본적으로 nlb는 클라이언트 ip를 보존하지 않으므로 request ip가 로드밸런서의 주소가 나온다.
또한 어노테이션으로 일일히 설정해준 덕분에 실제 타겟들의 헬스 상태를 콘솔에서도 체크할 수 있다.
이게 가능한 이유는 IP 모드를 썼기 때문으로, 로드밸런서가 실제 파드의 ip를 알고 헬스체크를 수행해주는 것이다.
NLB=$(k get svc svc-nlb-ip-type | awk 'NR==2 {print $4}')
for i in {1..100}; do curl -s $NLB | grep Hostname ; done | sort | uniq -c | sort -nr
로드밸런싱이 잘 되는지도 테스트해본다.
나온 출력을 정렬하면 고유값을 꺼내는 게 가능해져서 이렇게 편하게 결과를 확인할 수 있다.
k rollout deployment deploy-echo
ip 모드는 실제 파드의 상태를 체크하여 등록 해제와 새로운 등록을 알아서 해준다.
k get targetgroupbindings.elbv2.k8s.aws -o yaml
생성된 타겟그룹에 대한 정보를 오브젝트로 이렇게 확인할 수 있다.
간단하게 보안 그룹, vpc id까지도 확인 가능한 것이 보인다.
(그림은 다른 실습에서 따왔다)
추가적으로, 만약 ip모드가 아니라 인스턴스 모드로 로드밸런서를 만들면 타겟이 인스턴스로 설정된다.
타겟이 3개인 이유는 현재 실행 중인 노드가 3개이기 때문에 그렇다.
이때는 어디로는 트래픽이 일단 송신되고, 각 인스턴스의 iptables 룰에 의해 알아서 또 트래픽이 라우팅될 것이다.
타겟 그룹 속성 세팅
service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: >
deregistration_delay.timeout_seconds=60,
preserve_client_ip.enabled=true
이번에는 어노테이션으로 타겟 그룹에 대한 속성을 넣어보자.
대상 그룹에 대해 설정할 수 있는 것들은 전부 있다고 보면 된다.[4]
여러 개를 넣을 때는 이렇게 쉼표를 통해서 넣어주면 되는데, 그냥 넣으면 길어지니 >을 통해 folding 형태로 넣어주도록 한다.
이런 변경사항은 alb 컨트롤러가 계속 감시하며 어노테이션의 변경을 인지하고 동적으로 반영해준다.
설정한 값은 문제 생긴 대상을 빠르게 지워지도록 하는 60초 설정, 그리고 실제 client의 ip가 보존되도록 하는 기능이다.
이번에는 우리 집 ip가 나오는 것이 확인된다.
콘솔에서는 관련한 설정들이 제대로 들어간 것을 확인할 수 있다.
레디네스 게이트 활용
k label namespaces default elbv2.k8s.aws/pod-readiness-gate-inject=enabled
alb 컨트롤러에서 레디네스 게이트는 사용하고자 하는 네임스페이스에 설정해준다.
그럼 해당 네임스페이스에 배치되는 파드들에 readinessGates가 주입된다.
파드에 readiness gate가 설정되고, 이 값은 실제 로드 밸런서에서 헬스체크가 완료되었을 때 비로소 1이 된다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy-echo-2
spec:
replicas: 1
selector:
matchLabels:
app: deploy-websrv
template:
metadata:
labels:
app: deploy-websrv
spec:
terminationGracePeriodSeconds: 0
containers:
- name: aews-websrv
image: nginx
readinessProbe:
httpGet:
path: /
port: 80
만들어진 서비스에 엔드포인트로 잡히지만, 8080포트로 소켓을 열어두지 않는 임의의 파드를 하나 더 만들었다.
(참고로 containerPorts는 사실 트래픽에 아무런 영향을 주지 않는다.)
그러나 readiness 자체는 성공할 것이다.
보다시피 이 파드는 ready 상태이나, readiness gate는 계속 실패 상태이다.
마찬가지로 콘솔에서도 헬스체크는 실패 상태이다.
k get ep svc-nlb-ip-type -o yaml
이렇게 레디네스를 설정하면 실제로 서비스에서도 엔드포인트를 준비되지 않았다고 설정해준다.
readinessProbe는 사실 컨테이너 상태를 파악하는 kubelet의 요청에 불과하기에 실제 트래픽을 받을 수 있는 상태라고 확신할 수 없다.
그러나 readiness Gate를 활용하면 훨씬 안전하게 트래픽 대응이 가능하게 된다.
- 로드밸런서가 완전히 프로비저닝되어 헬스체크가 성공할 때까지 명시적으로 대기할 수 있다.
- 실제로 트래픽을 받을 수 없는 상태의 파드를 명확하게 걸러낼 수 있다.
뭐.. 사실 후자의 케이스는 홀로 관리하는 사람의 케이스라면 거의 일어나지 않을 이슈라고 생각한다.
하지만 분업 환경에서는 일어날 가능성은 있는 실수라는 생각이 든다.
이럴 때에도 readiness gate는 분명 도움이 되어줄 것이다.
여담으로, 레디네스 게이트가 설정되지 않은 네임스페이스에서는 보다시피 파드의 ready 상태만 따져서 엔드포인트가 추가된 것을 볼 수 있다.
k run -ti -n test debug --image nicolaka/netshoot -- zsh
---
for i in {1..100}; do
if curl -s svc-nlb-ip-type.test | grep -q "Hostname"; then
echo "success";
else
echo "fail";
fi;
done | sort | uniq -c | sort -nr
해당 네임스페이스에서 서비스로 계속 호출을 날려보면..
서비스가 간혹 실패하는 것이 보인다.
인그레스
k get ingressclass alb -oyaml | yh
로드밸런서 클래스와 다르게, 인그레스는 확실하게 클래스 오브젝트가 존재한다.
spec 하위 필드로 parameterts
필드를 넣음으로써 여러 설정이 가능한데, alb 컨트롤러에서는 이를 위해 IngressClassParams라는 CRD를 따로 제공한다.
parameters:
apiGroup: k8s.example.com
kind: IngressParameters
name: external-lb
세팅할 때는 이런 식으로 넣어주면 되는데, 현재 기본 클래스에는 없는 것이 보인다.
재밌는 게, 일단 기본으로 ingressclass params도 만들어 둔 게 보인다.
기본적인 실습을 해보자.
apiVersion: v1
kind: Namespace
metadata:
name: game-2048
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: game-2048
name: deployment-2048
spec:
selector:
matchLabels:
app.kubernetes.io/name: app-2048
replicas: 2
template:
metadata:
labels:
app.kubernetes.io/name: app-2048
spec:
containers:
- image: public.ecr.aws/l6m2t8p7/docker-2048:latest
imagePullPolicy: Always
name: app-2048
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
namespace: game-2048
name: service-2048
spec:
ports:
- port: 80
targetPort: 80
protocol: TCP
type: NodePort
selector:
app.kubernetes.io/name: app-2048
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
namespace: game-2048
name: ingress-2048
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
spec:
ingressClassName: alb
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service-2048
port:
number: 80
2048 게임을 배포해보자!
다시 말하지만 인그레스와 로드밸런서는 사용하는 어노테이션 이름이 다르다.
성공적으로 인그레스가 만들어진 것이 보인다.
브라우저로도 정상적으로 들어갈 수 있다.
보다시피 인그레스에 대해서는 alb가 만들어지는 것이 보인다.
인그레스 그룹 세팅
이번에는 여러 인그레스를 하나의 ALB에 묶어본다.
일전에 내가 미리 만들어둔 이미지를 활용해본다.
이 이미지는 자신의 현 버전 정보를 출력하도록 세팅됐다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: zero4
annotations:
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/group.name: aews
spec:
ingressClassName: alb
rules:
- http:
paths:
- path: /4
pathType: Prefix
backend:
service:
name: zero4
port:
number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: zero5
annotations:
alb.ingress.kubernetes.io/target-type: instance
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/group.name: aews
spec:
ingressClassName: alb
rules:
- http:
paths:
- path: /5
pathType: Prefix
backend:
service:
name: zero5
port:
number: 80
apiVersion: v1
kind: Service
metadata:
name: zero5
spec:
ports:
- port: 80
targetPort: 80
protocol: TCP
type: NodePort
selector:
app: zero5
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: zero5
spec:
selector:
matchLabels:
app: zero5
group: aews
replicas: 3
template:
metadata:
labels:
app: zero5
group: aews
spec:
containers:
- image: zerotay/zero-web:0.0.5
imagePullPolicy: Always
name: zero5
---
apiVersion: v1
kind: Service
metadata:
name: zero4
spec:
ports:
- port: 80
targetPort: 80
protocol: TCP
type: NodePort
selector:
app: zero4
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: zero4
spec:
selector:
matchLabels:
app: zero4
group: aews
replicas: 3
template:
metadata:
labels:
app: zero4
group: aews
spec:
containers:
- image: zerotay/zero-web:0.0.4
imagePullPolicy: Always
name: zero4
어노테이션으로 그룹을 넣어주면, 두 인그레스로 보여도 실제 리소스가 만들어질 때는 하나의 alb만이 만들어진다.
그룹 이름을 통해 로드 밸런서가 만들어졌으며, 뒷단에는 인스턴스와 ip 모드를 가진 서비스를 두고 있다.
4, 5를 경로로 설정했는데 둘다 제대로 값이 나오는 것을 볼 수 있다.
인그레스 파람
각종 설정들을 매번 해주는 것은 퍽 귀찮은 일이다.
어노테이션이란 것 자체가 애초에 만드는 시점에 유효성 검증을 하는 것도 어려워서 많은 혼란을 야기한다.
그래서 이걸 통합적으로 관리할 수 있도록 기본 인그레스 클래스에 조금의 설정을 넣자.
위에서 봤듯이 이미 인그레스 클래스는 있는 상태이다.
이 양식을 받아서 수정하고, 동시에 IngressClassParams 오브젝트를 만든다.
# Even if you apply this setting, ingress still needs to be set "name", "target-mode" FUCK
apiVersion: elbv2.k8s.aws/v1beta1
kind: IngressClassParams
metadata:
name: default-icp
spec:
scheme: internet-facing
group:
name: aews
loadBalancerAttributes:
- key: routing.http.xff_client_port.enabled
value: "true"
# certificateArn: ['arn:aws:acm:us-east-1:123456789:certificate/test-arn-1','arn:aws:acm:us-east-1:123456789:certificate/test-arn-2']
tags:
- key: org
value: aews
---
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
annotations:
meta.helm.sh/release-name: aws-load-balancer-controller
meta.helm.sh/release-namespace: kube-system
labels:
app.kubernetes.io/instance: aws-load-balancer-controller
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: aws-load-balancer-controller
app.kubernetes.io/version: v2.11.0
helm.sh/chart: aws-load-balancer-controller-1.11.0
name: alb
spec:
controller: ingress.k8s.aws/alb
parameters:
apiGroup: elbv2.k8s.aws
kind: IngressClassParams
name: default-icp
나는 이런 식으로 만들어주었다.
일단 이 클래스를 이용하면 그룹이 무조건 통일된다.
또한 ALB 헤더에 XFF를 추가해 클라의 ip를 보존할 수 있도록 세팅해주었다.
근데 타겟 모드, 도메인 이름은 결국 사용하는 리소스에서 어노테이션을 달긴해야 한다..
최소한 서브 도메인 postfix 관련 세팅이라도 있으면 좀더 세팅이 간편해질텐데.
설정을 적용하니 기존의 인그레스클래스에 parameter가 생기는 것이 보인다.
이 상태로 위에서 사용한 인그레스에서 그룹 관련 주석만 제거하고 다시 실행해본다.
내가 만든 이미지 0.0.6 버전은 XFF의 헤더 값이 있으면 이를 읽고 클라이언트 ip로서 출력한다.
인그레스 그룹 양식 파일을 조금 수정해서 다시 진행해본다.
0.0.4 버전을 0.0.6으로 바꾸고, 어노테이션에 그룹을 없앴다.
6버전에서는 클라이언트 ip로 내 로컬 주소가 나오는 것이 확인된다.
그룹 세팅이 이미 들어가있기에 하나의 로드밸런서에 두 인그레스가 묶였다.
External DNS
https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/aws.md
내가 사용하고 있는 도메인을 연결시킬 때 사용한다.
다양한 환경에서 적용할 수 있으며, 각 벤더마다 적용하는 문서가 잘..? 나와있다.
테라폼 세팅
나는 역시 테라폼을 이용해 세팅을 진행한다.
locals {
domain_name = "zerotay.com."
ext_name_in_cluster = "external-dns"
ext_namespace = "kube-system"
}
data "aws_route53_zone" "this" {
name = local.domain_name
private_zone = false
}
module "external_dns_irsa" {
source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
version = "5.52.2"
role_name = "external-dns"
attach_external_dns_policy = true
external_dns_hosted_zone_arns = [data.aws_route53_zone.this.arn]
force_detach_policies = true
oidc_providers = {
eks = {
provider_arn = data.aws_iam_openid_connect_provider.this.arn
namespace_service_accounts = ["kube-system:external-dns"]
}
}
}
resource "helm_release" "external_dns" {
name = local.ext_name_in_cluster
repository = "https://kubernetes-sigs.github.io/external-dns"
chart = "external-dns"
namespace = local.ext_namespace
values = [
<<-EOF
env:
- name: AWS_DEFAULT_REGION
value: ${data.aws_region.current.name}
provider:
name: aws
policy: upsert-only
serviceAccount:
annotations:
"eks.amazonaws.com/role-arn": ${module.external_dns_irsa.iam_role_arn}
EOF
]
}
세팅은 이렇게 해주었다.
일단 data로 현재 내가 가지고 있는 도메인의 각종 정보를 가져온다.
이후에 irsa를 설정을 해준 후에 헬름으로 배포!
values.yaml 파일에서 변경할 사항이 많다면 이렇게 그냥 직접적으로 values 파일을 통째로 넣는 방식이 괜찮은 것 같다.
일일히 set으로 하면 list, map을 넣을 때 매우 귀찮아진다.
더 베스트는 그냥 values.yaml 파일을 따로 만들어서 넣는 게 아닐까 싶기도 하나, 간단한 정도의 설정이라면 한 파일에서 관리하는 것이 조금 더 좋지 않을까 하는 생각이다.
테스트
성공적으로 컨트롤러가 설치됐다.
k get -n kube-system sa external-dns -o yaml | yh
irsa 설정도 제대로 들어간 것이 확인된다.
그럼 간단한 워크로드를 배포해서 확인해보자.
apiVersion: apps/v1
kind: Deployment
metadata:
name: tetris
labels:
app: tetris
spec:
replicas: 1
selector:
matchLabels:
app: tetris
template:
metadata:
labels:
app: tetris
spec:
containers:
- name: tetris
image: bsord/tetris
---
apiVersion: v1
kind: Service
metadata:
name: tetris
annotations:
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "http"
#service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "80"
spec:
selector:
app: tetris
ports:
- port: 80
protocol: TCP
targetPort: 80
type: LoadBalancer
loadBalancerClass: service.k8s.aws/nlb
기본 앱을 만들었다.
아직 external dns를 적용한 상태는 아니다.
DOMAIN=zerotay.com
kubectl annotate service tetris "external-dns.alpha.kubernetes.io/hostname=tetris.$DOMAIN"
어노테이션을 추가해본다.
적용하더라도 콘솔에서도, 클러스터에서도 해당 사실이 직접적으로 드러나진 않는다.
k -n kube-system logs external-dns...
하지만 설치된 컨트롤러의 로그를 보면 확실하게 동작을 한 것을 확인할 수 있다.
실제로도 잘 들어가는 것이 확인된다!
테라폼에서 적용하려면 이걸 참고할 수 있겠다.[5]
트래픽 정책 설정
노드 파드 생성 개수 제한
# kube-ops-view
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set service.main.type=LoadBalancer --set env.TZ="Asia/Seoul" --namespace kube-system
# kube-ops-view 접속 URL 확인 (1.5 배율)
kubectl get svc -n kube-system kube-ops-view -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' | awk '{ print "KUBE-OPS-VIEW URL = http://"$1":8080/#scale=1.5"}'
큐브옵스 뷰를 사용해서 확인해보자.
이걸 내가 확인해볼 수 있으면 좋을 것 같다.
https://velog.io/@xgro/KANS3-9week
t3 미디움에선 5 * 3으로 ip를 파드에 할당할 수 있으므로, 15개까지만 만들 수 있다.
로드밸런서.
어노테이션으로 deregistration을 설정할 수 있다.
이건 새로운 연결은 안 받고, 기존 연결을 유지하는 최대 시간.
스케일 다운을 할 때 유용한 설정이다.
vpc cni는 로밸이 바로 파드 ip로 보낼수도 있다는 점에서 엄청난 비용최적화를 이뤄낸다.
인그레스조차도 ip로 바로 접근..
이래서 ip모드가 좋은 것.
https://github.com/aws/aws-application-networking-k8s
aws의 게아도 해석해보자.
타겟그룹 바인딩을 crd로 두기 때문에 업그레이드를 자유롭게 할 수 있다.
관련 문서
이름 | noteType | created |
---|